﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Pfz.DataTypes;
using Pfz.Extensions;

namespace Pfz.Serialization
{
	/// <summary>
	/// Base class for BinarySerializer and RemotingSerializer.
	/// </summary>
	public abstract class BinarySerializerBase
	{
		#region DefaultTypes - Methods
			internal readonly HashSet<Assembly> _defaultAssemblies = new HashSet<Assembly>();
			internal readonly HashSet<Type> _defaultTypes = new HashSet<Type>();
			
			/// <summary>
			/// This method adds a type to the "automatic type list".
			/// This avoids such type to be saved in serialized streams, but the
			/// deserializer must add the exactly same types, in the exactly same
			/// order.
			/// Returns a boolean value indicating if such type was added (true),
			/// or if was already added before (false).
			/// </summary>
			public bool AddDefaultType(Type type)
			{
				if (type == null)
					throw new ArgumentNullException("type");
					
				// unnecessary checks, as the Type can be passed as Content.
				/*if (!type.IsSerializable)
					throw new ArgumentException("type must be serializable.\r\nType: " + type.FullName, "type");
					
				if (type.IsAbstract && type != typeof(Type))
					throw new ArgumentException("type must not be abstract.\r\nType: " + type.FullName, "type");*/
					
				_defaultAssemblies.Add(type.Assembly);
				bool result = _defaultTypes.Add(type);
				return result;
			}
			
			/// <summary>
			/// Adds primitive types as Default-Types.
			/// </summary>
			public void AddPrimitivesAsDefault()
			{
				_defaultAssemblies.Add(typeof(int).Assembly);
				_defaultTypes.Add(typeof(int));
				_defaultTypes.Add(typeof(long));
				_defaultTypes.Add(typeof(byte));
				_defaultTypes.Add(typeof(short));
				_defaultTypes.Add(typeof(uint));
				_defaultTypes.Add(typeof(ulong));
				_defaultTypes.Add(typeof(sbyte));
				_defaultTypes.Add(typeof(ushort));
				_defaultTypes.Add(typeof(bool));
				_defaultTypes.Add(typeof(char));
				_defaultTypes.Add(typeof(float));
				_defaultTypes.Add(typeof(double));
			}
			
			/// <summary>
			/// Adds primitives, string, DateTime, decimal and some other common
			/// types as default types.
			/// </summary>
			public void AddRecommendedDefaults()
			{
				AddPrimitivesAsDefault();
				_defaultAssemblies.Add(typeof(Date).Assembly);
				_defaultTypes.Add(typeof(string));
				_defaultTypes.Add(typeof(decimal));
				_defaultTypes.Add(typeof(Date));
				_defaultTypes.Add(typeof(DateTime));
				_defaultTypes.Add(typeof(Time));
				_defaultTypes.Add(typeof(Type));
				_defaultTypes.Add(typeof(byte[]));
				_defaultTypes.Add(typeof(int[]));
			}
			
			/// <summary>
			/// Adds the given type to the list of default types, and every
			/// type referenced by this type, directly or indirectly, that is
			/// not a value type (as the Type for value type references are 
			/// never serialized).
			/// </summary>
			public void AddDefaultTypeRecursive(Type type)
			{
				if (type == null)
					throw new ArgumentNullException("type");

				if (type.IsArray)
				{
					AddDefaultType(type);
					type = type.GetElementType();
				}

				if (!type.IsAbstract || type == typeof(Type))
					AddDefaultType(type);

				HashSet<Type> abstractTypes = new HashSet<Type>();
				_AddDefaultTypeRecursive(type, abstractTypes);
			}
			private void _AddDefaultTypeRecursive(Type type, HashSet<Type> abstractTypes)
			{
				if (type == typeof(Type))
					return;

				if (typeof(ISerializable).IsAssignableFrom(type))
					return;
					
				var fields = _GetFields(type);
				foreach(var field in fields)
				{
					Type fieldType = field.FieldType;

					if (fieldType.IsArray)
					{
						AddDefaultType(fieldType);
						fieldType = fieldType.GetElementType();
					}
					
					if ((fieldType.IsAbstract && fieldType != typeof(Type)) || (fieldType.IsValueType && (!fieldType.IsGenericType || fieldType.GetGenericTypeDefinition() != typeof(Nullable<>))))
					{
						if (!abstractTypes.Add(fieldType))
							continue;
					}
					else
					{
						if (!AddDefaultType(fieldType))
							continue;
					}
						
					_AddDefaultTypeRecursive(fieldType, abstractTypes);
				}
			}
			
			/// <summary>
			/// Add the nullable version of already added value-type defaults
			/// as default values.
			/// </summary>
			public void AddNullableOfDefaultsAsDefaults()
			{
				bool wasAdded = false;
				var array = _defaultTypes.ToArray();
				foreach(var item in array)
				{
					if (!item.IsValueType)
						continue;
					
					if (item.IsGenericType && item.GetGenericTypeDefinition() == typeof(Nullable<>))
						continue;
					
					var nullableType = typeof(Nullable<>).MakeGenericType(item);
					_defaultTypes.Add(nullableType);
					wasAdded = true;
				}
				
				if (wasAdded)
					_defaultAssemblies.Add(typeof(Nullable<>).Assembly);
			}
		#endregion

		#region _GetFields
			private static Dictionary<Type, FieldInfo[]> _fieldInfos = new Dictionary<Type, FieldInfo[]>();
			internal static FieldInfo[] _GetFields(Type type)
			{
				FieldInfo[] result;
				
				if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
					type = type.GetGenericArguments()[0];
				
				lock(_fieldInfos)
				{
					if (_fieldInfos.TryGetValue(type, out result))
						return result;
					
					List<FieldInfo> list = new List<FieldInfo>();
					Type actualType = type;
					while(actualType != null)
					{
						result = actualType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
						
						foreach(var field in result)
							if (!field.ContainsCustomAttribute<NonSerializedAttribute>() && !field.ContainsCustomAttribute<OptionalFieldAttribute>())
								list.Add(field);
						
						actualType = actualType.BaseType;
					}
					
					result = list.ToArray();
					_fieldInfos.Add(type, result);
				}

				return result;
			}
		#endregion
	}
}
